package com.service.impl;

import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.domain.Chunk;
import com.domain.LocalFile;
import com.domain.Video;
import com.dto.JsonResponse;
import com.mapper.ChunkDao;
import com.mapper.LocalFileDao;
import com.service.LocalFileService;
import com.service.VideoService;
import com.util.FastDFSUtil;
import com.util.FileUtilLocal;
import com.util.StringUtils;
import com.util.VideoUtil;
import io.netty.util.internal.StringUtil;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLEncoder;
import java.util.*;

@Service
public class LocalFileServiceImpl extends ServiceImpl<LocalFileDao, LocalFile> implements LocalFileService {

    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private LocalFileDao fileDao;

    @Autowired
    private ChunkDao chunkDao;

    @Autowired
    private VideoService videoService;

    @Autowired
    private FastDFSUtil fastDFSUtil;

    @Value("${ip}")
    String serverIp;

    private static final String ROOT_PATH = System.getProperty("user.dir") + "\\file\\";

    @Transactional(rollbackFor = Exception.class)
    @Override
    public String uploadPic(MultipartFile file, Integer type) throws IOException {
        if (file.isEmpty()) {
            throw new RuntimeException("上传的文件为空！");
        }
        // 若文件已存在，直接返回
        String multiFileMD5 = FileUtilLocal.getMultiFileMD5(file);
        LocalFile localFile = getLocalFileByMd5(multiFileMD5);
        if (localFile != null) {
            return localFile.getFileMd5();
        }
        getDirSize(new File(ROOT_PATH));

        // 获取文件基本信息
        String originalFilename = file.getOriginalFilename();
        String mainName = FileUtil.mainName(originalFilename);
        String realFileName = originalFilename;
        String fileSuffix = StringUtils.substring(originalFilename,
                StringUtils.lastIndexOf(originalFilename, ".") + 1);
        String fileType = "";
        switch (type) {
            case 1:
                fileType = "videoCover";
                break;
            case 2:
                fileType = "userCover";
                break;
            default:
                throw new RuntimeException("文件类型不正确！");
        }

        // 如果当前文件的父级目录不存在，就创建
        if (!FileUtil.exist(ROOT_PATH + fileType)) {
            FileUtil.mkdir(ROOT_PATH + fileType);
        }
        if (FileUtil.exist(ROOT_PATH + fileType + File.separator + originalFilename)) {
            realFileName = System.currentTimeMillis() + "_" + originalFilename;
        }
        String filePath = fileType + File.separator + realFileName;
        File saveFile = new File(ROOT_PATH + filePath);

        // 存入数据库
        LocalFile fileVideo = new LocalFile();
        fileVideo.setId(0L);
        fileVideo.setFileName(mainName);
        fileVideo.setFileType(fileType);
        fileVideo.setFileSuffix(fileSuffix);
        fileVideo.setFilePath(filePath);
        fileVideo.setFileMd5(multiFileMD5);
        fileVideo.setFileSize((int) file.getSize());
        fileVideo.setCreateTime(new Date());
        fileDao.insert(fileVideo);

        // 存储文件到本地的磁盘里去
        file.transferTo(saveFile);
        return multiFileMD5;
    }

    // 用于本地上传视频
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String uploadVideo(MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            throw new RuntimeException("上传的文件为空！");
        }
        // 若文件已存在，直接返回
        String multiFileMD5 = FileUtilLocal.getMultiFileMD5(file);
        LocalFile localFile = getLocalFileByMd5(multiFileMD5);
        if (localFile != null) {
            return localFile.getFileMd5();
        }
        getDirSize(new File(ROOT_PATH));

        // 单文件上传
        // 获取文件基本信息
        String originalFilename = file.getOriginalFilename();
        String mainName = FileUtil.mainName(originalFilename);
        String coverName = mainName + "_封面";
        String realFileName = originalFilename;
        String fileSuffix = StringUtils.substring(originalFilename,
                StringUtils.lastIndexOf(originalFilename, ".") + 1);
        String fileType = "video";

        // 校验后缀格式
        if ("mp4".equals(fileSuffix) || "webm".equals(fileSuffix) || "hlv".equals(fileSuffix)) {
        } else {
            throw new RuntimeException("文件格式错误！请上传 .mp4 .png .webm 格式的视频!");
        }

        // 如果当前文件的父级目录不存在，就创建
        if (!FileUtil.exist(ROOT_PATH + fileType)) {
            FileUtil.mkdir(ROOT_PATH + fileType);
        }
        if (FileUtil.exist(ROOT_PATH + fileType + File.separator + originalFilename)) {
            realFileName = System.currentTimeMillis() + "_" + originalFilename;
        }
        String filePath = fileType + File.separator + realFileName;
        File saveFile = new File(ROOT_PATH + filePath);
        // 存储文件到本地的磁盘里去
        file.transferTo(saveFile);

        // 生成封面
        LocalFile fetchCover = null;
        Integer videoTime = 0;
        try {
            fetchCover = VideoUtil.fetchCover(saveFile, coverName);
            videoTime = VideoUtil.getVideoTime(saveFile);
        } catch (Exception e) {
            FileUtil.del(saveFile);
            e.printStackTrace();
        }
        // 模拟添加视频
        Video video = new Video();
        video.setUserId(1L);
        video.setVideoUrl(multiFileMD5);
        video.setVideoCover(fetchCover.getFileMd5());
        video.setVideoTitle(mainName);
        video.setVideoType("1");
        video.setVideoDuration(videoTime);
        video.setVideoPartition("fashion");
        video.setVideoIntroduction("测试");
        video.setCreateTime(new Date());
        video.setVideoPlay(0);
        videoService.addVideo(video);

        // 存入数据库
        LocalFile fileVideo = new LocalFile();
        fileVideo.setFileName(mainName);
        fileVideo.setFileType(fileType);
        fileVideo.setFileSuffix(fileSuffix);
        fileVideo.setFilePath(filePath);
        fileVideo.setFileDuration(videoTime);
        fileVideo.setFileMd5(multiFileMD5);
        fileVideo.setFileSize((int) file.getSize());
        fileVideo.setCreateTime(new Date());
        fileDao.insert(fileVideo);
        fileDao.insert(fetchCover);
        return multiFileMD5;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public String uploadChunk(MultipartFile file, Chunk chunk) {
        // 首先检查是否有完整的文件
        QueryWrapper<LocalFile> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.lambda().eq(LocalFile::getFileMd5, chunk.getMd5());
        if (fileDao.exists(fileQueryWrapper)) {
            return "200";
        }

        String[] splits = chunk.getFileName().split("\\.");
        String fileSuffix = splits[splits.length - 1];
        String filePath = "video" + File.separator + chunk.getMd5() + "." + fileSuffix;
        String resultFileName = ROOT_PATH + File.separator + filePath;
        getDirSize(new File(ROOT_PATH));
        writeChunk(file, chunk, resultFileName);

        if (Objects.equals(chunk.getChunkIndex(), chunk.getChunkTotal())) {
            // 查询是否已全部上传
            QueryWrapper<Chunk> chunkQueryWrapper = new QueryWrapper<>();
            chunkQueryWrapper.lambda().eq(Chunk::getMd5, chunk.getMd5());
            List<Chunk> chunkList = chunkDao.selectList(chunkQueryWrapper);
            File saveFile = new File(ROOT_PATH + filePath);

            // 若所有分片都上传成功
            if (chunkList.size() >= chunk.getChunkTotal()) {
                // 存入数据库
                LocalFile fileVideo = new LocalFile();
                int end = chunk.getFileName().length() - fileSuffix.length() - 1;
                fileVideo.setFileName(chunk.getFileName().substring(0, end));
                fileVideo.setFileType("video");
                fileVideo.setFileSuffix(fileSuffix);
                fileVideo.setFilePath(filePath);
                fileVideo.setFileDuration(VideoUtil.getVideoTime(saveFile));
                fileVideo.setFileMd5(chunk.getMd5());
                fileVideo.setFileSize((int) FileUtil.size(saveFile));
                fileVideo.setCreateTime(new Date());
                fileDao.insert(fileVideo);

                // 删除所有分片记录
                QueryWrapper<Chunk> chunkQueryWrapper2 = new QueryWrapper<>();
                chunkQueryWrapper2.lambda().eq(Chunk::getMd5, chunk.getMd5());
                chunkDao.delete(chunkQueryWrapper2);
                return "200";
            } else {
                return "500";
            }
        } else {
            return "201";
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void download(String multiFileMD5, HttpServletResponse response) throws IOException {
        LocalFile localFile = getLocalFileByMd5(multiFileMD5);
        if (localFile == null) {
            throw new RuntimeException("文件不存在!");
        }
        String fileRealPath = ROOT_PATH + localFile.getFilePath();
        if (!FileUtil.exist(fileRealPath)) {
            throw new RuntimeException("文件不存在!");
        }
        byte[] bytes = FileUtil.readBytes(fileRealPath);

//        response.addHeader("Content-type", "video/mp4");
        //获取文件的mimeType
        String mimeType = FileUtil.getMimeType(fileRealPath);
        response.setContentType(mimeType);
//        response.setContentType("application/x-download");
        //给浏览器设置响应头：Content-Disposition   告诉浏览器以附件的形式打开这个文件
        String encode = URLEncoder.encode(localFile.getFileName(), "UTF-8");
        response.setHeader("Content-Disposition", "attachment;fileName=" + encode);

//        postman下载的文件名是response.mp4，不用管，是postman的bug
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void downloadVideo(String multiFileMD5, HttpServletRequest request, HttpServletResponse response) throws IOException {
        LocalFile localFile = getLocalFileByMd5(multiFileMD5);
        if (localFile == null) {
            throw new RuntimeException("文件不存在!");
        }
        String fileRealPath = ROOT_PATH + localFile.getFilePath();
        if (!FileUtil.exist(fileRealPath)) {
            throw new RuntimeException("文件不存在!");
        }
        // 计算视频流的起止位置
        String rangeStr = request.getHeader("Range");
        String[] range;
        Integer totalFileSize = localFile.getFileSize();
        if (StringUtil.isNullOrEmpty(rangeStr)) {
            rangeStr = "bytes=0-" + (totalFileSize - 1);
        }
        range = rangeStr.split("bytes=|-");
        long begin = 0;
        if (range.length >= 2) {
            begin = Long.parseLong(range[1]);
        }
        long end = totalFileSize - 1;
        if (range.length >= 3) {
            end = Long.parseLong(range[2]);
        }
        long len = end - begin + 1;
        String contentRange = "bytes " + begin + "-" + end + "/" + totalFileSize;
        byte[] chunk = readChunk(begin, (int) len, ROOT_PATH + localFile.getFilePath());

        response.setHeader("Content-Range", contentRange);
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Type", "video/mp4");
        response.setContentLength((int) len);
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        OutputStream outputStream = response.getOutputStream();
        outputStream.write(chunk);
        outputStream.flush();
        outputStream.close();
    }

    /**
     * 根据文件的MD5密文获取文件实体类对象
     */
    @Override
    public LocalFile getLocalFileByMd5(String multiFileMD5) {
        // 根据md5查询文件
        QueryWrapper<LocalFile> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.lambda().eq(LocalFile::getFileMd5, multiFileMD5);
        return fileDao.selectOne(fileQueryWrapper);
    }

    public JsonResponse<Map<String, Object>> checkChunk(String md5) {
        // 首先检查是否有完整的文件
        QueryWrapper<LocalFile> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.lambda().eq(LocalFile::getFileMd5, md5);
        boolean isUploaded = fileDao.exists(fileQueryWrapper);

        Map<String, Object> data = new HashMap<>();
        data.put("isUploaded", isUploaded);
        // 如果有，就返回秒传
        if (isUploaded) {
            return new JsonResponse<>("201", "文件已经秒传", data);
        }

        // 如果没有，就查找分片信息，并返回给前端
        QueryWrapper<Chunk> chunkQueryWrapper = new QueryWrapper<>();
        chunkQueryWrapper.lambda().eq(Chunk::getMd5, md5);
        List<Chunk> chunkList = chunkDao.selectList(chunkQueryWrapper);
        if (!chunkList.isEmpty()) {
            // 转成数组格式
            List<Integer> indexList = new ArrayList<>();
            chunkList.forEach(index -> indexList.add(index.getChunkIndex()));
            Integer[] chunks = new Integer[indexList.size()];
            indexList.toArray(chunks);
            data.put("chunkList", chunks);
            return new JsonResponse<>("201", "文件已经秒传", data);
        } else {
            data.put("chunkList", new ArrayList<>());
            return new JsonResponse<>("200", "文件还未传", data);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public String mergeChunk(Chunk chunk) {
        // 查找分片信息，判断是否要合并
        QueryWrapper<Chunk> chunkQueryWrapper = new QueryWrapper<>();
        chunkQueryWrapper.lambda().eq(Chunk::getMd5, chunk.getMd5());
        List<Chunk> chunkList = chunkDao.selectList(chunkQueryWrapper);

        if (chunkList.size() == chunk.getChunkTotal()) {
            String[] splits = chunk.getFileName().split("\\.");
            String fileSuffix = splits[splits.length - 1];
            int end = chunk.getFileName().length() - fileSuffix.length() - 1;
            String filePath = "video" + File.separator + chunk.getMd5() + "." + fileSuffix;
            File saveFile = new File(ROOT_PATH + filePath);
            // 存入数据库
            LocalFile fileVideo = new LocalFile();
            fileVideo.setFileName(chunk.getFileName().substring(0, end));
            fileVideo.setFileType("video");
            fileVideo.setFileSuffix(fileSuffix);
            fileVideo.setFilePath(filePath);
            fileVideo.setFileDuration(VideoUtil.getVideoTime(saveFile));
            fileVideo.setFileMd5(chunk.getMd5());
            fileVideo.setFileSize((int) FileUtil.size(saveFile));
            fileVideo.setCreateTime(new Date());
            fileDao.insert(fileVideo);
            // 删除所有分片记录
            QueryWrapper<Chunk> chunkQueryWrapper2 = new QueryWrapper<>();
            chunkQueryWrapper2.lambda().eq(Chunk::getMd5, chunk.getMd5());
            chunkDao.delete(chunkQueryWrapper2);
        }
        return "201";
    }


    private void getDirSize(File dir) {
        // 判断文件夹不超过100G
        long size = FileUtils.sizeOfDirectory(dir);
        System.out.println(size);
        if (size > 100 * 1024 * 1024 * 1024L) {
            throw new RuntimeException("文件夹已满，请联系管理员！");
        }
    }

    private void writeChunk(MultipartFile file, Chunk chunk, String resultFileName) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw")) {
            // 偏移量
            long offset = chunk.getChunkSize() * (chunk.getChunkIndex() - 1);
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            // 写入
            randomAccessFile.write(file.getBytes());
            // 记录分片信息
            Chunk chunkSave = new Chunk();
            chunkSave.setId(0L);
            chunkSave.setMd5(chunk.getMd5());
            chunkSave.setChunkIndex(chunk.getChunkIndex());
            chunkDao.insert(chunkSave);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public byte[] readChunk(long offset, Integer chunkSize, String resultFileName) {
//        File resultFile = new File(resultFileName);
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "r")) {
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            // 读取
            byte[] buffer = new byte[chunkSize];
            randomAccessFile.read(buffer);
            return buffer;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 返回上传路径 , 断点续传 + 秒传 + 视频信息录入数据库
     * slice：当前分片
     * fileMd5：文件的md5值
     * sliceNo：当前分片属于第几个
     * totalSliceNo：总共要上传几个分片
     */
    @Transactional(rollbackFor = Exception.class)
    public String uploadFileBySlices(MultipartFile slice, String fileMD5,
                                     Integer sliceNo, Integer totalSliceNo) throws IOException {
        LocalFile dbFileMD5 = getLocalFileByMd5(fileMD5);
        getDirSize(new File(ROOT_PATH));

        //如果不是null，说明之前已经传过这个文件了，那就不用重复上传，直接返回文件路径就好了
        if (dbFileMD5 != null) {
            return dbFileMD5.getFilePath();
        }

        //之前没传过这个文件，现在要把文件上传至文件服务器，并返回上传路径
        String url = fastDFSUtil.uploadFileBySlices(slice, fileMD5, sliceNo, totalSliceNo);
        if (!StringUtil.isNullOrEmpty(url)) {
            dbFileMD5 = new LocalFile();
            dbFileMD5.setFileMd5(fileMD5);
            dbFileMD5.setFilePath(url);
            dbFileMD5.setFileType(fastDFSUtil.getFileType(slice));
            dbFileMD5.setCreateTime(new Date());
            fileDao.insert(dbFileMD5);
        }
        return url;
    }

}
